# -*- coding: utf-8 -*-
import os, stat, errno
import fuse
import sys
from fuse import Fuse, Stat, FuseStruct, FuseInvalidate
from inode import Inode
import time
import pdb

class Simple_Direntry(FuseStruct):
    """
    Auxiliary class for carrying directory entry data.
    Initialized with 'name' and 'ino'.
    Further attributes:

    offset
            An integer (or long) parameter, used as a bookmark
            during directory traversal.
            This needs to be set it you want stateful directory
            reading.

    type
            Directory entry type, should be one of the stat type
            specifiers (stat.S_IFLNK, stat.S_IFBLK, stat.S_IFDIR,
            stat.S_IFCHR, stat.S_IFREG, stat.S_IFIFO, stat.S_IFSOCK).

    Note that Python's standard directory reading interface is
    stateless and provides only names, so the above optional
    attributes doesn't make sense in that context.
    """
    def __init__(self, name, offset, type, ino, **kw):
        self.name   = name
        self.offset = offset
        self.type   = type
        self.ino    = ino

        FuseStruct.__init__(self, **kw)

class Simple(Fuse):
    """
    Filesystem class. The principal task is manage the filesystem and
            administer the log.
            Note: The log can be deactive just not calling the function 'open_log'
    """
    def __init__(self, *argv, **kw):
        fuse.Fuse.__init__(self, *argv, **kw)
        # File descriptor for print.
        self.__fd = None
        # Loglvl of print.
        self.__loglvl = None
        # List of free ino number to assing
        self.__free_inonumber = []
        # Small ino number.
        self.__inonumber = 1
        # Table of path,inode.
        self.__table = {}


    def open_log(self, filename = None, loglvl = [1]):
        """
        His task is set the type and lvl of log.
        Default argument; filename = stdout, loglvl = [1]
        More:
                Filename; Can be any fd. only needed the absolut path.
                Loglvl; A list of levels: 1, 2 and 3 are the options.
        """
        if filename == None:
                self.__fd = sys.stdout
        else:
                self.__fd = open(filename, 'w')
        self.__loglvl = loglvl

    def debug1(self, format, *others):
        self.__debug(1,format,*others)

    def debug2(self, format, *others):
        self.__debug(2,format,*others)

    def debug3(self, format, *others):
        self.__debug(3,format,*others)

    def __debug(self,lvl,format,*others):
        if (self.__fd != None and self.__loglvl != None):
            if not (lvl in self.__loglvl) : return
            t = time.strftime("%a, %d %b %Y %H:%M:%S -0300", time.gmtime())
            self.__fd.write('[%s] ' % t)
            self.__fd.write((format + '\n') % others)
            self.__fd.flush()

    # Determined if an user has permission on a file.
    def __has_permission(self, path):
        # Take the inode from the table.
        inode = self.__table[path]
        # Take the uid.
        uid = fuse.os.getuid()
        if(uid == inode.metadata.st_uid):
            return True # If the user has permission on the file.
        else:
            return False # Otherwise.

    def __check_path_long(self,path):
        fname = path.rpartition('/')[2]
        return len (fname) > 255

    def getattr(self, path):
        self.debug1('Getattr-Entry: Path: %s', path)
        # If path is root.
        if (path == '/'):
            st = Stat()
            st.st_mode = stat.S_IFDIR | 0755
            st.st_nlink = 2
        # Else check the file exist.
        elif (self.__table.has_key(path)):
            st = self.__table[path].metadata
        else:
            st = -errno.ENOENT
        self.debug1('Getattr-Exit: St: %s', st)
        return st

    def mknod(self, path, mode, dev):
        self.debug1('Mknod-Entry: Path: %s, Mode: %s, Dev: %s', path, mode, dev)
        if self.__check_path_long(path):
            self.debug1('Mknod-Exit: %s', -errno.ENAMETOOLONG)
            return -errno.ENAMETOOLONG
        # Take a new time for actualize the time's.
        new_time = time.time()
        # Instantiate a inode.
        inode = Inode()
        # Path of the father of file.
        p_father = path.rpartition('/')[0]
        # If path no is root then;
        if (p_father != ''):
            # Take the inode of father
            i_father = self.__table[p_father]
            # Update the time's
            i_father.metadata.st_mtime = new_time
            i_father.metadata.st_ctime = new_time
        # Set metadatas.
        inode.metadata.st_nlink = 1
        inode.metadata.st_dev = dev
        inode.metadata.st_mode = mode
        inode.metadata.st_atime = new_time
        inode.metadata.st_mtime = new_time
        inode.metadata.st_ctime = new_time
        inode.metadata.st_uid = fuse.os.getuid()
        inode.metadata.st_gid = fuse.os.getgid()
        # If not exist ino number to reuse.
        if(self.__free_inonumber == []):
            # Set ino number.
            inode.metadata.st_ino = self.__inonumber
            # Ingrease the default ino number.
            self.__inonumber += 1
        else:
            # Take the first(and slower) ino number free.
            inode.metadata.st_ino = self.__free_inonumber[0]
            # Remove the ino number of the list of free.
            self.__free_inonumber.remove(self.__free_inonumber[0])
        # Add the path,inode to table.
        self.__table[path] = inode
        self.debug1('Mknod-Exit: %s', 0)
        return 0

    def readdir(self, path, offset):
        self.debug1('Readdir-Entry: Path: %s, Offset: %s', path, offset)
        result = {}
        # If path not is root.
        if path != "/":
            path = path + "/"
        # Take the list of path's.
        list_paths = self.__table.keys()
        for i in list_paths:
            # If path (i) start with path.
            if i.startswith (path):
                # Take; name from path/name.
                name = i.partition (path)[2]
                # If name isnt a dir.
                if (name.find('/') == -1):
                    # Take the inode of path; path+name.
                    inode = self.__table[path + name]
                    # Take the ino number.
                    st_ino = inode.metadata.st_ino
                    # Add the name,st_ino to result.
                    result[name] = st_ino
        self.debug1('Readdir-Exit: Collected list of path\'s: %s', result)
        for r in result:
            # For each name,st_inode, Instantiate of Simple_Direntry.
            yield Simple_Direntry(r, 0, 0, result[r])

    def write(self, path, buff, offset):
        self.debug1('Write-Entry: Path: %s, Offset: %s', path, offset)
        self.debug3('Write-Entry: Path: %s, Buff: %s, Offset: %s', path, buff[0:len(buff)-1], offset)
        # If i have permission.
        if(self.__has_permission(path)):
            # Size of the info to write.
            size = len(buff)
            # Take a new time for actualize the time's.
            new_time = time.time()
            # Take the inode from the table.
            inode = self.__table[path]
            # If the file size is not enough.
            if (inode.metadata.st_size < size + offset):
                i_size = size + offset
                # Truncate file.
                self.truncate (path, i_size)
            # Take the first part of the data in the file.
            first = inode.data[0 : offset]
            # Take the last part of the data in the file
            last = inode.data[(size + offset) : inode.metadata.st_size]
            # Clean the data.
            inode.data = ''
            # Conform the new data in the file.
            inode.data = first + buff + last
            # Update the time's
            inode.metadata.st_mtime = new_time
            inode.metadata.st_atime = new_time
            self.debug3('Write-Exit: Data: %s', inode.data)
            rt = size
        else:
            # Permission denied.
            rt = -errno.EACCES

        self.debug1('Write-Exit: Ret: %s', rt)
        return rt

    def read (self, path, length, offset):
        self.debug1('Read-Entry: Path: %s, Lenght: %s, Offset: %s', path, length, offset)
        # If i have permission.
        if(self.__has_permission(path)):
            # Take a new time for actualize the time's.
            new_time = time.time()
            # Take the inode from the table.
            inode = self.__table[path]
            # Update the time.
            inode.metadata.st_atime = new_time
            # Take the data from inode.
            data = inode.data
            # Read from data.
            buff = data[offset : length + offset]
            rt = buff
            prt = len(buff)
            self.debug1('Read-Exit: Size: %s', prt)
            self.debug3('Read-Exit: Buff: %s', rt)
        else:
            # Permission denied.
            rt = -errno.EACCES

        self.debug1('Read-Exit: Ret: %s', prt)
        return rt

    def utime (self, path, times):
        self.debug1('Utime-Entry: Path: %s, Times: %s', path, times)
        # If i have permission.
        if(self.__has_permission(path)):
            # Take a new time for actualize the time's.
            new_time = time.time()
            # Take the inode from the table.
            inode = self.__table[path]
            # Update the time's.
            inode.metadata.st_ctime = new_time
            inode.metadata.st_mtime = times[1]
            inode.metadata.st_atime = times[0]
            self.debug1('Utime-Exit: Ret: %s', 0)
        else:
            # Permission denied.
            rt = -errno.ENOTEMPTY
        return 0

    def mkdir(self, path, mode):
        self.debug1('Mkdir-Entry: Path: %s, Mode: %s', path, mode)
        if self.__check_path_long(path):
            self.debug1('Mkdir-Exit: %s', -errno.ENAMETOOLONG)
            return -errno.ENAMETOOLONG
        # If dir not exist.
        if (not (self.__table.has_key(path))):
            # Create a dir.
            self.mknod(path, (mode | stat.S_IFDIR), 0)
            ret = 0
        else:
            # Dir exist.
            ret = -errno.EEXIST
        self.debug1('Mkdir-Exit: Ret: %s', ret)
        return ret

    def rmdir(self, path):
        self.debug1('Rmdir-Entry: Path: %s', path)
        # Take a new time for actualize the time's.
        new_time = time.time()
        # Path with / at the end.
        extended_path = path + '/'
        # Path of father to path.
        father = path.rpartition('/')[0]
        # If path isn't /
        if(not(father == '')):
            # Take the inode father.
            inode_father = self.__table[father]
            # Update the time's.
            inode_father.metadata.st_ctime = new_time
            inode_father.metadata.st_mtime = new_time
        # Take the list of path's.
        key_list = self.__table.keys()
        count = 0
        for key in key_list:
            if(key.startswith(extended_path)):
                    count = count + 1
        # If the dir is empty.
        if(count == 0):
            # Take the inode from table.
            inode = self.__table[path]
            # Set the ino number of inode for use.
            self.__free_inonumber.append(inode.metadata.st_ino)
            # Erase the path,inode from table.
            self.__table.pop(path)
            ret = 0
        else:
            # Dir isn't empty
            ret = -errno.ENOTEMPTY
        self.debug1('Rmdir-Exit: Ret: %s', ret)
        return ret

    def unlink (self, path):
        self.debug1('Unlink-Entry: Path: %s', path)
        # Take a new time for actualize the time's.
        new_time = time.time()
        # Path of father to path.
        father = path.rpartition('/')[0]
        # If path isn't /
        if father != '':
            # Take the inode of father.
            inode_father = self.__table[father]
            # Update the time's.
            inode_father.metadata.st_mtime = new_time
            inode_father.metadata.st_ctime = new_time
        # Take the inode of path.
        inode = self.__table[path]
        # Reduce the number of link of inode.
        inode.metadata.st_nlink = inode.metadata.st_nlink - 1
        # Update the time.
        inode.metadata.st_ctime = new_time
        # Remove path,inode from table.
        del self.__table[path]
        # If link in inode is 0.
        if(inode.metadata.st_nlink == 0):
            # Set the ino number of inode for use.
            self.__free_inonumber.append(inode.metadata.st_ino)
        self.debug1('Unlink-Exit: Ret: %s', 0)
        return 0

    def rename (self, current_path, new_path):
        self.debug1('Unlink-Entry: Current_Path: %s, New_Path: %s', current_path, new_path)
        # If i have permission.
        if(self.__has_permission(current_path)):
            # Take a new time for actualize the time's.
            new_time = time.time()
            # Take inode of path from table.
            inode = self.__table[current_path]
            # Remove current_path,inode from table.
            del self.__table[current_path]
            # Add new_path,inode to table.
            self.__table[new_path] = inode
            # Update the time's.
            inode.metadata.st_mtime = new_time
            inode.metadata.st_ctime = new_time
            inode.metadata.st_atime = new_time
        else:
            # Permission denied.
            return -errno.EACCES
        self.debug1('Unlink-Exit: Ret: %s', 0)
        return 0

    def chmod (self, path, mode):
        self.debug1('Chmod-Entry: Path: %s, Mode: %s', path, mode)
        if self.__check_path_long(path):
            self.debug1('Chmod-Exit: %s', -errno.ENAMETOOLONG)
            return -errno.ENAMETOOLONG
        # Take a new time for actualize the time's.
        new_time = time.time()
        # Take inode of path from table.
        inode = self.__table[path]
        # Update mode.
        inode.metadata.st_mode = mode
        # Update the time.
        inode.metadata.st_ctime = new_time
        
        self.debug1('Chmod-Exit: Ret: %s', 0)
        return 0

    def chown (self, path, uid, gid):
        self.debug1('Chown-Entry: Path: %s, Uid: %s, Gid: %s', path, uid, gid)
        if self.__check_path_long(path):
            self.debug1('Chown-Exit: %s', -errno.ENAMETOOLONG)
            return -errno.ENAMETOOLONG
        # Take a new time for actualize the time's.
        new_time = time.time()
        # Take inode of path from table.
        inode = self.__table[path]
        # Valid Uid.
        if (uid >= 0):
            # Update Uid.
            inode.metadata.st_uid = uid
        # Valid Gid.
        if (gid >= 0):
            # Update Gid.
            inode.metadata.st_gid = gid
        # Update the time.
        inode.metadata.st_ctime = new_time
        self.debug1('Chown-Exit: Ret: %s', 0)
        return 0

    def truncate (self, path, new_size):
        self.debug1('Truncate-Entry: Path: %s, New_Size: %s', path, new_size)
        # If i have permission.
        if(self.__has_permission(path)):
            # Take a new time for actualize the time's.
            new_time = time.time()
            # Take inode of path from table.
            inode = self.__table[path]
            # Take current size from inode.
            current_size = inode.metadata.st_size
            if (new_size != current_size):
                try:
                    # Make a list of \0.
                    zeros = ''.join(['\0' for i in xrange(new_size - current_size)])
                    # Update the data.
                    inode.data = inode.data + zeros
                except MemoryError:
                    # No space.
                    rt = -errno.ENOSPC
                    self.debug1('Truncate-Exit: Ret: %s', ret)
                    return rt
                # Update the size.
                inode.metadata.st_size = new_size
                # Update the time's.
                inode.metadata.st_mtime = new_time
                inode.metadata.st_ctime = new_time
                inode.metadata.st_atime = new_time
            rt = 0
        else:
            # Permission denied.
            rt = -errno.EACCES

        self.debug1('Truncate-Exit: Ret: %s', rt)
        return rt

    def readlink (self, path):
        self.debug1('Readlink: path: %s', path)
        # If i have permission.
        if(self.__has_permission(path)):
            # Take inode of path from table.
            inode = self.__table[path]
            # Take mode from inode.
            st_mode = inode.metadata.st_mode
            # Is a sym link.
            if (stat.S_ISLNK(st_mode)):
                # Take data from inode.
                buf = inode.data
                rt  = buf
            else:
                # Inappropriate link.
                rt = -EXDEV
        else:
            # Permission denied.
            rt = -errno.EACCES

        self.debug1('Readlink-Exit: Ret: %s', rt)
        return rt

    def symlink (self, target, name):
        self.debug1('Symlink-Entry: Target: %s, name: %s', target, name)
        aux = '/' + target
        exist = self.__table.has_key(aux)
        # If file exist.
        if (exist):
            # If i have permission.
            if(self.__has_permission(aux)):
                # Make a sym link.
                self.mknod(name, stat.S_IFLNK, 0)
                # Add a name,inode to table.
                inode = self.__table[name]
                # Save the target of the original inode in data.
                inode.data = target
                rt = 0
            else:
                # Permission denied.
                rt = -errno.EACCES
        else:
            # Make a sym link.
            self.mknod(name, stat.S_IFLNK, 0)
            # Add a name,inode to table.
            inode = self.__table[name]
            # Save the target of the original inode in data.
            inode.data = target
            rt = 0
        self.debug1('Symlink-Exit: Ret: %s', rt)
        return rt

    def link (self, target, name):
        self.debug1('Hardlink-Entry: Target: %s, name: %s', target, name)
        # If i have permission.
        if(self.__has_permission(target)):
            # Take a new time for actualize the time's.
            new_time = time.time()
            # Take inode of path from table.
            inode = self.__table[target]
            # Path of the father of file.
            p_father = target.rpartition('/')[0]
            # If path no is root then;
            if (p_father != ''):
                # Take the inode of father
                i_father = self.__table[p_father]
                # Update the time's
                i_father.metadata.st_mtime = new_time
                i_father.metadata.st_ctime = new_time
            # Take the number of link of inode.
            st_nlink = inode.metadata.st_nlink
            # Increase in one the number of link.
            inode.metadata.st_nlink = st_nlink + 1
            # Update the time's.
            inode.metadata.st_atime = new_time
            inode.metadata.st_ctime = new_time
            # Add name,inode to table.
            self.__table[name] = inode
            rt = 0
        else:
            # Permission denied.
            rt = -errno.EACCES

        self.debug1('Hardlink-Exit: Ret: %s', 0)
        return rt

    def statfs(self):
        self.debug1('Statfs-Entry')

        result = fuse.StatVfs()

        result.f_frsize  = 4
        result.f_blocks  = 1
        result.f_bfree   = 3
        result.f_bavail  = 7
        result.f_files   = 4
        result.f_ffree   = 8
        result.f_favail  = 0
        result.f_flag    = 5
        result.f_namemax = 20

        self.debug1('Statfs-Exit: Ret: %s', result)
        return result
